"""
Configuration-Driven Runner

A runner that executes test steps defined in extension configuration files.
"""

import os
import subprocess
import shlex
from pathlib import Path
from typing import Dict, List, Optional, Any
import logging

from .base_runner import BaseRunner, ExecutionResult
from .config_parser import ExtensionDescriptor, TestStep

logger = logging.getLogger(__name__)


class TestResult:
    """Result of a test execution."""
    
    def __init__(self, success: bool, message: str = "", output: str = "", 
                 artifacts: Optional[List[str]] = None):
        self.success = success
        self.message = message
        self.output = output
        self.artifacts = artifacts or []
        
    def __bool__(self) -> bool:
        return self.success


class ConfigDrivenRunner(BaseRunner):
    """
    Runner that executes test steps from configuration descriptors.
    
    This runner reads test instructions from extension configuration files
    and executes them sequentially, providing a way to define test logic
    without writing Python code.
    """
    
    def __init__(self, extension_name: str, descriptor: ExtensionDescriptor):
        """
        Initialize the configuration-driven runner.
        
        Args:
            extension_name: Name of the extension this runner belongs to
            descriptor: Extension descriptor containing test configuration
        """
        super().__init__(extension_name)
        self.descriptor = descriptor
    
    def can_run(self, executable_path: Path) -> bool:
        """
        Check if this runner can execute the given binary.
        
        Args:
            executable_path: Path to executable
            
        Returns:
            True if runner can handle this executable
        """
        if not executable_path.exists():
            return False
        
        # Check if we have any test steps defined
        if not self.descriptor.test_steps:
            logger.debug(f"No test steps defined for extension {self.extension_name}")
            return False
        
        # Check if executable matches our patterns
        source_path = executable_path.parent
        
        # If no source patterns defined, assume we can run
        if not self.descriptor.source_patterns:
            return True
        
        # Check if any source patterns match
        for pattern in self.descriptor.source_patterns:
            if self._matches_pattern(source_path, pattern) or self._matches_pattern(executable_path, pattern):
                logger.debug(f"Executable path {executable_path} matches pattern: {pattern}")
                return True
        
        logger.debug(f"Executable path {executable_path} doesn't match any patterns: {self.descriptor.source_patterns}")
        return False
    
    def run(self, executable_path: Path, config_file: Optional[Path] = None,
            run_config: Optional[Dict[str, Any]] = None, 
            timeout: Optional[int] = None) -> ExecutionResult:
        """
        Execute the solver framework using configured test steps.
        
        Args:
            executable_path: Path to solver executable
            config_file: Path to solver configuration file
            run_config: Runtime configuration parameters
            timeout: Execution timeout in seconds
            
        Returns:
            ExecutionResult with execution details
        """
        import time
        start_time = time.time()
        
        # Determine source path from executable
        source_path = executable_path.parent
        
        # Run the configured tests
        test_result = self.run_tests(source_path, run_config, verbose=run_config.get('verbose', False) if run_config else False)
        
        execution_time = time.time() - start_time
        
        return ExecutionResult(
            success=test_result.success,
            return_code=0 if test_result.success else 1,
            stdout=test_result.output,
            stderr="",
            execution_time=execution_time
        )
    
    def get_command_line(self, executable_path: Path, config_file: Optional[Path] = None,
                        run_config: Optional[Dict[str, Any]] = None) -> List[str]:
        """
        Get the command line for executing the solver.
        
        For config-driven runners, this returns the first test step command.
        
        Args:
            executable_path: Path to solver executable
            config_file: Path to solver configuration file  
            run_config: Runtime configuration parameters
            
        Returns:
            Command line as list of strings
        """
        if not self.descriptor.test_steps:
            return [str(executable_path)]
        
        # Return the first test step command (expanded)
        first_step = self.descriptor.test_steps[0]
        command = first_step.command
        
        # Simple command line parsing - could be enhanced
        import shlex
        return shlex.split(command)
    
    def run_tests(self, source_path: Path, test_config: Optional[Dict[str, Any]] = None,
                  verbose: bool = False) -> TestResult:
        """
        Run tests using configured test steps.
        
        Args:
            source_path: Path to source directory
            test_config: Test configuration parameters
            verbose: Whether to enable verbose output
            
        Returns:
            TestResult indicating success/failure and artifacts
        """
        logger.info(f"Starting tests for {self.extension_name} at {source_path}")
        
        if not self.descriptor.test_steps:
            logger.warning(f"No test steps defined for extension {self.extension_name}")
            return TestResult(success=True, message="No test steps defined", artifacts=[])
        
        # Prepare environment
        env = self._prepare_environment(source_path, test_config)
        
        # Execute test steps
        artifacts = []
        all_output = []
        
        for i, step in enumerate(self.descriptor.test_steps):
            logger.info(f"Executing test step {i+1}/{len(self.descriptor.test_steps)}: {step.name}")
            
            try:
                result = self._execute_test_step(step, source_path, env, verbose)
                all_output.append(f"=== Test Step: {step.name} ===\n{result.output}")
                
                if not result.success:
                    logger.error(f"Test step '{step.name}' failed: {result.message}")
                    return TestResult(
                        success=False,
                        message=f"Tests failed at step '{step.name}': {result.message}",
                        output="\n\n".join(all_output),
                        artifacts=artifacts
                    )
                
                # Collect any artifacts from this step
                artifacts.extend(result.artifacts)
                
            except Exception as e:
                logger.error(f"Exception in test step '{step.name}': {e}")
                return TestResult(
                    success=False,
                    message=f"Tests failed at step '{step.name}': {str(e)}",
                    output="\n\n".join(all_output),
                    artifacts=artifacts
                )
        
        # Add configured test artifacts
        configured_artifacts = self.get_test_artifacts(source_path)
        artifacts.extend(configured_artifacts)
        
        logger.info(f"Tests completed successfully for {self.extension_name}")
        return TestResult(
            success=True,
            message=f"All {len(self.descriptor.test_steps)} test steps passed",
            output="\n\n".join(all_output),
            artifacts=list(set(artifacts))  # Remove duplicates
        )
    
    def get_test_artifacts(self, source_path: Path) -> List[str]:
        """
        Get list of test artifacts from configuration.
        
        Args:
            source_path: Path to source directory
            
        Returns:
            List of artifact paths relative to source_path
        """
        artifacts = []
        
        # Add configured artifacts
        for artifact_pattern in self.descriptor.test_artifacts:
            # Expand environment variables and resolve paths
            expanded_pattern = os.path.expandvars(artifact_pattern)
            artifact_path = source_path / expanded_pattern
            
            if artifact_path.exists():
                # Make path relative to source_path
                try:
                    relative_path = artifact_path.relative_to(source_path)
                    artifacts.append(str(relative_path))
                except ValueError:
                    # Path is not relative to source_path, use absolute
                    artifacts.append(str(artifact_path))
            else:
                logger.debug(f"Test artifact not found: {artifact_path}")
        
        return artifacts
    
    def validate_test_environment(self, source_path: Path) -> List[str]:
        """
        Validate that the test environment is ready.
        
        Args:
            source_path: Path to source directory
            
        Returns:
            List of error messages (empty if valid)
        """
        errors = []
        
        # Check if required commands are available
        commands_to_check = set()
        for step in self.descriptor.test_steps:
            # Extract the command (first word) from the command string
            command_parts = shlex.split(step.command)
            if command_parts:
                commands_to_check.add(command_parts[0])
        
        for command in commands_to_check:
            if not self._command_exists(command):
                errors.append(f"Required command '{command}' not found in PATH")
        
        # Check if source path exists and is accessible
        if not source_path.exists():
            errors.append(f"Source path does not exist: {source_path}")
        elif not os.access(source_path, os.R_OK):
            errors.append(f"Source path is not readable: {source_path}")
        
        return errors
    
    def get_test_info(self, source_path: Path) -> Dict[str, Any]:
        """
        Get information about the test configuration.
        
        Args:
            source_path: Path to source directory
            
        Returns:
            Dictionary with test information
        """
        return {
            'runner': self.__class__.__name__,
            'extension': self.extension_name,
            'source_path': str(source_path),
            'descriptor_name': self.descriptor.name,
            'descriptor_version': self.descriptor.version,
            'test_steps_count': len(self.descriptor.test_steps),
            'test_artifacts_count': len(self.descriptor.test_artifacts),
            'supported_frameworks': self.descriptor.supported_frameworks,
        }
    
    def _prepare_environment(self, source_path: Path, test_config: Optional[Dict[str, Any]]) -> Dict[str, str]:
        """Prepare environment variables for test execution."""
        env = os.environ.copy()
        
        # Add build environment (tests might need build tools)
        env.update(self.descriptor.build_environment)
        
        # Add test config environment if provided
        if test_config and 'environment' in test_config:
            env.update(test_config['environment'])
        
        # Set common variables
        env['GSM_SOURCE_PATH'] = str(source_path)
        env['GSM_EXTENSION_NAME'] = self.extension_name
        
        return env
    
    def _execute_test_step(self, step: TestStep, source_path: Path, 
                          base_env: Dict[str, str], verbose: bool) -> TestResult:
        """Execute a single test step."""
        
        # Prepare environment for this step
        env = base_env.copy()
        if step.environment:
            env.update(step.environment)
        
        # Determine working directory
        if step.working_dir:
            working_dir = Path(os.path.expandvars(step.working_dir))
            if not working_dir.is_absolute():
                working_dir = source_path / working_dir
        else:
            working_dir = source_path
        
        logger.debug(f"Executing test command: {step.command}")
        logger.debug(f"Working directory: {working_dir}")
        
        try:
            # Execute the command
            process = subprocess.run(
                step.command,
                shell=True,
                cwd=working_dir,
                env=env,
                capture_output=True,
                text=True,
                timeout=step.timeout
            )
            
            output = ""
            if process.stdout:
                output += f"STDOUT:\n{process.stdout}\n"
            if process.stderr:
                output += f"STDERR:\n{process.stderr}\n"
            
            if verbose:
                logger.info(f"Test step '{step.name}' output:\n{output}")
            
            artifacts = []
            
            # Check for expected files if specified
            if step.expected_files:
                for expected_file in step.expected_files:
                    file_path = working_dir / os.path.expandvars(expected_file)
                    if file_path.exists():
                        try:
                            relative_path = file_path.relative_to(source_path)
                            artifacts.append(str(relative_path))
                        except ValueError:
                            artifacts.append(str(file_path))
                    else:
                        logger.warning(f"Expected file not found: {file_path}")
            
            if process.returncode == 0:
                return TestResult(
                    success=True, 
                    message=f"Test step '{step.name}' passed",
                    output=output,
                    artifacts=artifacts
                )
            else:
                return TestResult(
                    success=False,
                    message=f"Test step '{step.name}' failed with exit code {process.returncode}",
                    output=output,
                    artifacts=artifacts
                )
        
        except subprocess.TimeoutExpired:
            return TestResult(
                success=False,
                message=f"Test step '{step.name}' timed out after {step.timeout} seconds",
                output="Test timed out"
            )
        except Exception as e:
            return TestResult(
                success=False,
                message=f"Test step '{step.name}' failed with exception: {str(e)}",
                output=str(e)
            )
    
    def _matches_pattern(self, source_path: Path, pattern: str) -> bool:
        """Check if source path matches the given pattern."""
        import fnmatch
        
        # Check if pattern matches directory name
        if fnmatch.fnmatch(source_path.name, pattern):
            return True
        
        # Check if pattern matches any file in the directory
        for file_path in source_path.rglob('*'):
            if fnmatch.fnmatch(file_path.name, pattern):
                return True
            # Also check relative path from source
            try:
                rel_path = file_path.relative_to(source_path)
                if fnmatch.fnmatch(str(rel_path), pattern):
                    return True
            except ValueError:
                continue
        
        return False
    
    def _command_exists(self, command: str) -> bool:
        """Check if a command exists in the PATH."""
        import shutil
        return shutil.which(command) is not None